﻿
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Mail;
using System.Text;
using System.Timers;

namespace EmperialApps.WeatherSpark.Service.Internal {

    /// <summary>Manages the server log.</summary>
    internal sealed class LogManager : IDisposable {

        /// <summary>Initializes a new instance of the <see cref="LogManager"/> class.</summary>
        public LogManager( string logDirectory, string serverName ) {
            TraceEventType.Verbose.Trace( "Initializing log manager with directory: " + logDirectory + ", " + serverName );
            this._logDirectory = logDirectory;
            this._serverName = serverName;

            this._timer = new Timer { Interval = LogTransferInterval.TotalMilliseconds };
            this._timer.Elapsed += this.OnTimerElapsed;
            this._timer.Enabled = true;

            AppDomain.CurrentDomain.UnhandledException += this.OnUnhandledException;

            this._listener = new ServerTraceListener( this.OnLogSizeMaximized );
            this._listener.LogPath = this.GetLogPath( );
            Trace.Listeners.Add( this._listener );
        }

        /// <inheritdoc/>
        public void Dispose( ) {
            this._disposed = true;
            TraceEventType.Information.Trace( "Disposing log manager." );
            this.TransferLog( this._listener.LogSize, false );

            this._timer.Enabled = false;
            this._timer.Dispose( );
        }

        #region Private Members

        private const string TraceLogPattern = "WeatherSpark.TraceLog{0}.txt";

        private const int DateLength = 10;
        private const int FullDatePrefixLength = DateLength + 10;

        private const string FailedForecastEvent = "Forecast=()";
        private const string VerbosePrefix = "WS Verbose:";
        private const string InformationPrefix = "WS Information:";
        private static readonly string[] InformationDigestExclusions = new[] { "Creating", "Forecast", "Lazily", "Processing", "Received", "Saving", "Ignoring" };

#if DEBUG
        private const int MaximumLogSize = 8192;
        private static readonly TimeSpan LogTransferInterval = TimeSpan.FromMinutes( 10 );
#else
        private const int MaximumLogSize = ushort.MaxValue;
        private static readonly TimeSpan LogTransferInterval = TimeSpan.FromDays( 0.7 );
#endif

        private readonly string _serverName;
        private readonly string _logDirectory;
        private readonly ServerTraceListener _listener;
        private readonly Timer _timer;

        private int _lastLogNumber;
        private bool _disposed;

        private static bool IncludeInDigest( string line ) {
            if( string.IsNullOrWhiteSpace( line ) )
                return false;

            if( line.Length <= FullDatePrefixLength )
                return true;

            string fullMessage = line.Substring( FullDatePrefixLength );

            if( fullMessage.StartsWith( VerbosePrefix ) )
                return false;

            if( !fullMessage.StartsWith( InformationPrefix ) )
                return true;

            string message = fullMessage.Substring( InformationPrefix.Length ).TrimStart( );
            bool exclude = !message.Contains( FailedForecastEvent )
                        && InformationDigestExclusions.Any( message.StartsWith );
            return !exclude;
        }

        private static string FormatForDigest( string line ) {
            string formattedLine;
            if( !char.IsDigit( line, 0 ) )
                formattedLine = "&nbsp; " + line;
            else if( line.EndsWith( "File did not contain any temperature data." )
                  || line.Contains( FailedForecastEvent ) )
                formattedLine = "<em>" + line + "</em>";
            else
                formattedLine = "<strong>" + line + "</strong>";

            return formattedLine;
        }

        private string GetLogPath( ) {
            string logPath;
            do {
                ++this._lastLogNumber;
                string logName = string.Format( TraceLogPattern, this._lastLogNumber );
                logPath = Path.Combine( this._logDirectory, logName );
            } while( File.Exists( logPath ) );

            return logPath;
        }

        private void OnTimerElapsed( object sender, ElapsedEventArgs e ) {
            if( this._disposed )
                return;

            TraceEventType.Verbose.Trace( "Performing scheduled log transfer." );
            this.TransferLog( this._listener.LogSize, true );
        }

        private int OnLogSizeMaximized( int size ) {
            if( this._disposed )
                return size;

            this._timer.Enabled = false;
            this.TransferLog( size, true );
            this._timer.Enabled = true;
            return 0;
        }

        private void OnUnhandledException( object sender, UnhandledExceptionEventArgs e ) {
            var ex = e.ExceptionObject as Exception;
            if( ex != null )
                ex.Report( (e.IsTerminating ? "" : "Non-") + "Terminating Unhandled Exception", wait: true );

            this.TransferLog( this._listener.LogSize, !e.IsTerminating );
        }

        private void TransferLog( int size, bool continueLogging ) {
#if TRACE
            try {
                // Save current log location and update listener.
                this._listener.LogSize = 0;
                string nextLogPath = continueLogging ? this.GetLogPath( ) : null;
                string previousLogPath = this._listener.LogPath;
                this._listener.LogPath = nextLogPath;

                // Transfer previous log file.
                if( previousLogPath != null ) {
                    string subject = string.Format( "{0}Server Log {1} |{2:0.0}KiB| {3}",
                        continueLogging ? "" : "End ",
                        DateTime.Now.ToUniversalString( ).Substring( 0, DateLength ),
                        Math.Max( 0.1, size / 1024.0 ),
                        this._serverName );
                    var lines =
                        File.ReadLines( previousLogPath )
                            .Where( IncludeInDigest )
                            .Select( System.Net.WebUtility.HtmlEncode )
                            .Select( FormatForDigest );
                    string digest = string.Join( "<br/>", lines );

                    Action complete = delegate {
                        // Clear sent log.
                        TraceEventType.Verbose.Trace( "DELETING: " + previousLogPath );
                        try { File.Delete( previousLogPath ); }
                        catch( IOException ex ) { ex.Trace( "Error deleting old log: " + previousLogPath ); }
                    };

                    var log = new Attachment( previousLogPath );
                    var message = new MailMessage { Body = digest, Attachments = { log }, IsBodyHtml = true };
                    message.Report( subject, complete, wait: !continueLogging );
                }
            }
            catch( Exception ex ) {
                ex.Report( "Log Transfer Failure", wait: true );
            }
#endif
        }


        private sealed class ServerTraceListener : TextWriterTraceListener {
            private readonly Func<int, int> _logSizeMaximized;
            private string _logPath;
            private int _logSize;

            public ServerTraceListener( Func<int, int> logSizeMaximized ) {
                this._logSizeMaximized = logSizeMaximized;
            }

            public string LogPath {
                get { return this._logPath; }
                set {
                    // Flush old writer, if any.
                    using( this.Writer )
                        this._logPath = null;

                    // Set new writer.
                    this.Writer =
                        value == null
                            ? null
                            : new StreamWriter( value, false, Encoding.UTF8 );
                    this._logPath = value;
                }
            }

            public int LogSize {
                get { return this._logSize; }
                set {
                    if( value < MaximumLogSize ) {
                        this._logSize = value;
                    }
                    else {
                        this._logSize = 0;
                        this._logSize += this._logSizeMaximized( value );
                    }
                }
            }

            public override void Write( string message ) {
                this.FilterMessage( message );
            }

            public override void WriteLine( string message ) {
                if( this.FilterMessage( message ) )
                    base.WriteLine( "" );
            }

            private bool FilterMessage( string message ) {
                if( this.Writer == null || message.Contains( "PASS" ) )
                    return false;

                base.Write( message );

                if( IncludeInDigest( message ) )
                    this.LogSize += message.Length;
                return true;
            }
#if DEBUG
            public override string ToString( ) {
                return this._logPath;
            }
#endif
        }

        #endregion
    }

}
